/*! Copyright (C) 2009 Apertus, All Rights Reserved *! Author : Apertus Team *! *! This program is free software: you can redistribute it and/or modify *! it under the terms of the GNU General Public License as published by *! the Free Software Foundation, either version 3 of the License, or *! (at your option) any later version. *! *! This program is distributed in the hope that it will be useful, *! but WITHOUT ANY WARRANTY; without even the implied warranty of *! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *! GNU General Public License for more details. *! *! You should have received a copy of the GNU General Public License *! along with this program. If not, see <http://www.gnu.org/licenses/>. *! -----------------------------------------------------------------------------**/ import java.io.IOException; import java.io.File; import java.util.logging.Level; import java.util.logging.Logger; import javax.sound.sampled.DataLine; import javax.sound.sampled.TargetDataLine; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.AudioFileFormat; import javax.sound.sampled.Line; import javax.sound.sampled.Mixer; import javax.sound.sampled.Mixer.Info; public class AudioRecorder extends Thread { private TargetDataLine Dataline; private AudioInputStream AudioInputStream; private AudioInputStream AudioMonitorStream; private AudioFileFormat.Type RecAudioFileFormat; private AudioFormat RecAudioFormat; private File AudioFile; ElphelVision Parent; private Thread Monitor; private Thread Recorder; private boolean MonitorRunning = false; private int MonitorCyclesPerSecond = 100; private int RecorderCyclesPerSecond = 5; private boolean Recording = false; private int MixerID = -1, FormatID = -1; public AudioRecorder(ElphelVision parent) { Parent = parent; Monitor = new Thread(this); Recorder = new Thread(this); } public void StartRecording() { MonitorRunning = true; Recording = true; if (!Dataline.isActive()) { Dataline.start(); } if (!Monitor.isAlive()) { Monitor.start(); } if (!Recorder.isAlive()) { Recorder.start(); } Parent.WriteLogtoConsole("Audio Recording started"); } public void StopRecording() { Dataline.stop(); //Dataline.close(); //MonitorRunning = false; //Recording = false; Parent.WriteLogtoConsole("Audio Recording stopped"); } // called once all data has been read from Audiobuffer after stopping the recording private void PostStopRecording() { Dataline.close(); // once writing finished close the Dataline Recording = false; // set state after the above function finished when targetdataline does not provide any new data SetAudioOptions(); //reinit for next recording //We need to close the Dataline to end the recording, but we still want to monitor it afterwards so we need to start the monitor again StartMonitor(); } public void StartMonitor() { if (!Dataline.isActive()) { Dataline.start(); } if (!Monitor.isAlive()) { Monitor.start(); } MonitorRunning = true; Parent.WriteLogtoConsole("Audio Monitoring started"); } public synchronized void SetFilename(String filename) { AudioFile = new File(filename); } public String[] GetAvailableAudioMixers() { // Get all mixers from the system - USB audio device will have its own mixer Info[] mixerinfo = AudioSystem.getMixerInfo(); String[] Return = new String[mixerinfo.length]; for (int i = 0; i < mixerinfo.length; i++) { AudioFormat[] formats = GetMixerCapabilities(i); if (formats != null) { Return[i] = mixerinfo[i].getName() + " " + mixerinfo[i].getDescription(); } } return Return; } public AudioFormat[] GetMixerCapabilities(int MixerIndex) { Info[] mixerinfo = AudioSystem.getMixerInfo(); // select the Mixer to record from Mixer mixer = AudioSystem.getMixer(mixerinfo[MixerIndex]); Line.Info[] Infos = mixer.getTargetLineInfo(); for (int i = 0; i < Infos.length; i++) { if (Infos[i] instanceof DataLine.Info) { DataLine.Info dataLineInfo = (DataLine.Info) Infos[i]; // these are the available formats of the selected mixer AudioFormat[] supportedFormats = dataLineInfo.getFormats(); return supportedFormats; } } return null; } public synchronized void SetAudioOptions() { if ((FormatID != -1) && (MixerID != -1)) { SetAudioOptions(MixerID, FormatID); } } public void SetAudioOptions(int MixerIndex, int FormatIndex) { FormatID = FormatIndex; MixerID = MixerIndex; Info[] mixerinfo = AudioSystem.getMixerInfo(); // select the mixer to record from Mixer mixer = AudioSystem.getMixer(mixerinfo[MixerIndex]); // get all available audio formats on that device AudioFormat[] supportedFormats = GetMixerCapabilities(MixerIndex); // we use WAV by default RecAudioFileFormat = AudioFileFormat.Type.WAVE; // Create AudioFormat that the audio hardware supports // 48KHz is hardcoded for now until we create a custom field in the settings for it if (supportedFormats[FormatIndex].getSampleRate() == -1) { RecAudioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 48000.0F, supportedFormats[FormatIndex].getSampleSizeInBits(), supportedFormats[FormatIndex].getChannels(), supportedFormats[FormatIndex].getFrameSize(), 48000.0F, supportedFormats[FormatIndex].isBigEndian()); } else { RecAudioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, supportedFormats[FormatIndex].getSampleRate(), supportedFormats[FormatIndex].getSampleSizeInBits(), supportedFormats[FormatIndex].getChannels(), supportedFormats[FormatIndex].getFrameSize(), supportedFormats[FormatIndex].getSampleRate(), supportedFormats[FormatIndex].isBigEndian()); } TargetDataLine targetDataLine = null; try { targetDataLine = AudioSystem.getTargetDataLine(RecAudioFormat, mixerinfo[MixerIndex]); targetDataLine.open(RecAudioFormat); } catch (LineUnavailableException e) { Parent.WriteErrortoConsole("unable to get a recording line"); } Dataline = targetDataLine; AudioInputStream = new AudioInputStream(Dataline); AudioMonitorStream = new AudioInputStream(Dataline); } private final float a2 = -1.9556f; private final float a3 = 0.9565f; private final float b1 = 0.9780f; private final float b2 = -1.9561f; private final float b3 = 0.9780f; private double peak; private static final double log10 = Math.log(10.0); private static final double maxDB = Math.max(0.0, 20.0 * Math.log((double) Short.MAX_VALUE) / log10); private final int peakHoldTime = 1000; private long then = System.currentTimeMillis(); private double rms; private double average; @Override public void run() { while (Thread.currentThread() == Monitor) { if (MonitorRunning) { byte[] Byte = new byte[1024]; int count = 0; try { while ((count = AudioMonitorStream.read(Byte)) != -1) { //Parent.WriteLogtoConsole("AudioMonitorStream.available(): " + AudioMonitorStream.available()); //Parent.WriteLogtoConsole("count: " + count); short[] samples = new short[count / 2]; for (int i = 0; i < count / 2; i++) { // 16 bit mode int offset = i * 2; samples[i] = (short) ((Byte[offset + 1] << 8) | (0x000000FF & Byte[offset])); //Parent.WriteLogtoConsole("i: " + i + " sample: " + samples[i]); // TODO: deal with any other than 16 bit mode } float energy = 0.0f; average = 0.0f; double y1 = 0.0f; double y2 = 0.0f; for (int i = 0; i < samples.length; i++) { short i1 = samples[i]; double j = 0; double k = 0; if (i > 0) { j = samples[i - 1]; } if (i > 1) { k = samples[i - 2]; } double y = b1 * i1 + b2 * j + b3 * k - a2 * y1 - a3 * y2; y2 = y1; y1 = y; double v2 = Math.abs(y); long now = System.currentTimeMillis(); energy += v2 * v2; average += v2; if (v2 > peak) { peak = v2; } else if ((now - then) > peakHoldTime) { peak = v2; then = now; } } rms = energy / samples.length; rms = Math.sqrt(rms); average /= samples.length; // Parent.WriteLogtoConsole("length: " + samples.length + " rms: " + rms + " average: " + average); } } catch (IOException ex) { Logger.getLogger(AudioRecorder.class.getName()).log(Level.SEVERE, null, ex); } try { Thread.sleep((int) (1.0f / MonitorCyclesPerSecond * 1000.0f)); } catch (InterruptedException e) { break; } } } while (Thread.currentThread() == Recorder) { if (Recording) { try { Parent.WriteLogtoConsole("Audio Writing started"); AudioSystem.write(AudioInputStream, RecAudioFileFormat, AudioFile); PostStopRecording(); } catch (IOException ex) { Logger.getLogger(AudioRecorder.class.getName()).log(Level.SEVERE, null, ex); } } else { try { Thread.sleep((int) (1.0f / RecorderCyclesPerSecond * 1000.0f)); } catch (InterruptedException e) { break; } } } } public final synchronized double getRmsDB() { return Math.max(0.0, 20.0 * Math.log(rms) / log10); } public final synchronized double getAverageDB() { return Math.max(0.0, 20.0 * Math.log(average) / log10); } public final synchronized double getPeakDB() { return Math.max(0.0, 20.0 * Math.log(peak) / log10); } public final synchronized boolean getIsClipping() { return (Short.MAX_VALUE) < (2 * peak); } public final synchronized double getMaxDB() { return maxDB; } } /* * How can I detect a buffer underrun or overrun? The following is working reliably at least with the "Direct Audio Device" mixers: SourceDataLine: underrun if (line.available() == line.getBufferSize()) SourceDataLine.available(): how much data can be written to the buffer. If the whole buffer can be written to, there is no data in the buffer to be rendered. TargetDataLine: overrun if (line.available() == line.getBufferSize()) TargetDataLine.available(): how much data can be read from the buffer. If the whole buffer can be read, there is no space in the buffer for new data captured from the line. */